[Objective-C] KVC 和 KVO


KVC Key Value Coding

KVC是一种用间接方式访问类的属性的机制。比如你要给一个类中的属性赋值或者取值,可以直接通过类和点运算符实现,当然也可以使用KVC。不过对于私有属性,点运算符就不起作用,因为私有属性不暴露给调用者,不过使用KVC却依然可以实现对私有属性的读写。

先看一下KVC的一部分源码,当然只能看到头文件:

// NSKeyValueCoding.h

@interface NSObject(NSKeyValueCoding)

+ (BOOL)accessInstanceVariablesDirectly;

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

@end


@interface NSArray(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end


@interface NSDictionary(NSKeyValueCoding)

- (nullable ObjectType)valueForKey:(NSString *)key;

@end


@interface NSMutableDictionary(NSKeyValueCoding)

- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end


@interface NSOrderedSet(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
- (void)setValue:(nullable id)value forKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

@end


@interface NSSet(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

可以看到这个类里面包含了对类NSObject,NSArray,NSDictionary,NSMutableDictionary,NSOrderedSet,NSSet的拓展。拓展的方法基本上为

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

也就是说,基本上Objective-C里所有的对象都支持KVC操作,操作包含如上两类方法,动态读取和动态设值。

好多地方说是NSObject实现了NSKeyValueCoding协议。而代码里是类的拓展。这两种说法是相通的嘛?

举个🌰,新建一个Command line程序:

// Account.h
@interface Account : NSObject

@property (nonatomic, assign) float balance;

@end

// Account.m
@implementation Account {
    float salaryPerDay;
}

@synthesize balance = _balance;

- (void)setBalance:(float)balance {
    NSLog(@"set balance invoked");
    _balance = balance;
}

- (float)balance {
    NSLog(@"get balance invoked");
    return _balance;
}

@end

// Person.h
@class Account;

@interface Person : NSObject {
    @private
    int _age;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) Account *account;

- (void)showMessage;

@end

// Person.m
#import "Person.h"

@implementation Person {
    NSString *_sex;
}

- (void)showMessage {
    NSLog(@"name = %@, age = %d, sex = %@", _name, _age, _sex);
}

@end

// main
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person1 = [[Person alloc] init];
        [person1 setValue:@"Wossoneri" forKey:@"name"];
        [person1 setValue:@25 forKey:@"age"];       //私有变量也可以访问
        [person1 setValue:@"male" forKey:@"sex"];   //私有变量也可以访问

        [person1 showMessage];

        Account *account1 = [[Account alloc] init];
        person1.account = account1;

        [person1 setValue:@1000.0 forKeyPath:@"account.balance"];
        [person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];

        NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
        NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);

    }
    return 0;
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
Person1`s balance is : 1000.00
Person1`s salary is : 300.00

代码说明:

  • Person类里用旧方法声明私有变量_age以及直接添加的私有成员变量_sex,同时声明一个开放的属性_name
  • 对于_name,O-C会直接为其生成对应的settergetter,所以可以通过点运算符操作属性,比如
    person1.name = @"Wossoneri";
    
  • 可以看到KVC可以对私有变量进行操作。对于当前类的直接成员变量,把变量名作为key来访问,否则要写成keyPath来访问。
  • KVC运行时首先会优先调用属性的gettersetter,这一点可以在代码输出的第二行和第三行看到,如果没有,就会优先搜索_property,不存在则搜索property,如果仍然没有,就会调用setValue:forUndefinedKey:valueForUndefinedKey:方法

KVO Key Value Observing

介绍

KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。

更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知

<NSKeyValueObserving> 或者 KVO 是一个非正式的协议,该协议定义了一个观察和通知对象之间状态变化的通用机制。作为一个非正式的协议,你在使用该协议的类中看不到惯用的写法<NSKeyValueObserving>,实际上,这个协议只是隐式地由NSObject实现,继承NSObject的子类默认能够使用这个协议。

放一部分NSKeyValueObserving.h对于NSObject的拓展代码

@interface NSObject(NSKeyValueObserving)

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;

@end

@interface NSObject(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

从拓展名称就可以看出,使用KVO需要注册监听器,也需要删除监听器。监听过程需要使用observeValueForKeyPath回调方法。
所以使用方法就可以推测出个大概来:

  1. addObserver方法注册一个监听器
  2. 复写observeValueForKeyPath回调,获得监听到的信息,做对应操作。
  3. 使用结束removeObserver,这很重要。

实现

最后对上面代码做一些改动,我需要对Account对象的balance做监听,当balance内容改变,我要做输出处理。

#pragma mark - For KVO
- (void)setAccount:(Account *)account {
    _account = account;
    //add observer
    [_account addObserver:self
               forKeyPath:@"balance"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
}

//override
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    if ([keyPath isEqualToString:@"balance"]) {
        NSLog(@"keyPath = %@, object = %@, newValue = %.2f, context = %@", keyPath, object, [[change objectForKey:@"new"] floatValue], context);
    }
}

- (void)dealloc {
    [_account removeObserver:self forKeyPath:@"balance"];
}

//main
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person1 = [[Person alloc] init];
        [person1 setValue:@"Wossoneri" forKey:@"name"];
        [person1 setValue:@25 forKey:@"age"];       //私有变量也可以访问
        [person1 setValue:@"male" forKey:@"sex"];   //私有变量也可以访问

        [person1 showMessage];

        Account *account1 = [[Account alloc] init];
        person1.account = account1;

        [person1 setValue:@1000.0 forKeyPath:@"account.balance"];
        [person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];


        //KVO
        account1.balance = 4000.0;

        NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
        NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
keyPath = balance, object = , newValue = 1000.00, context = (null)
set balance invoked
get balance invoked
keyPath = balance, object = , newValue = 4000.00, context = (null)
get balance invoked
Person1`s balance is : 4000.00
Person1`s salary is : 300.00

优点

当有属性改变,KVO会提供自动的消息通知。这样的架构有很多好处。首先,开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。这是KVO机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。其次,KVO的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。

Swift的KVO与KVC

Swift版本的的就看这篇文章吧,内容很详细。
漫谈 KVC 与 KVO

Reference
iOS开发系列—Objective-C之KVC、KVO


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录